﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using Fuse8.DomainFramework.Domain;
using Fuse8.DomainFramework.Practices.Domain;
using Fuse8.DomainFramework.Practices.Domain.Interfaces;
using Fuse8.DomainFramework.Practices.SystemFacade.Extensions;

namespace Fuse8.DomainFramework.Practices.SystemFacade
{
	public static class EntityGenerator
	{
		public static readonly ModuleBuilder ModuleBuilder;

		public static readonly Dictionary<Type, Type> InterfaceMap = new Dictionary<Type, Type>
		        {
		            {typeof (IEntityObject<Int32>), typeof (Int32EntityObject)},
		            {typeof (IEntityObject<Guid>), typeof (GuidEntityObject)},
		            {typeof (IEntityObject<String>), typeof (StringEntityObject)},
		        };

		static EntityGenerator()
		{
			var assemblyBuilder =
				AppDomain.CurrentDomain.DefineDynamicAssembly(
				new AssemblyName("MemoryTableDynamicAssembly"),
				AssemblyBuilderAccess.Run);

			ModuleBuilder =
				assemblyBuilder.DefineDynamicModule("MemoryTableDynamicModule");

            AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
            { 
                if (args.Name == "MemoryTableDynamicAssembly")
                {
                    return assemblyBuilder;
                }
                return null;
            };
		}

		private static int _convertToRuntimeMethodCount = 0;
		public static MethodInfo ConvertToRuntimeMethod(LambdaExpression expression)
		{
			var typeBuilder = ModuleBuilder.DefineType(String.Format("Run_{0}:{1}", _convertToRuntimeMethodCount++, expression), TypeAttributes.Class | TypeAttributes.Public);
			var methodBuilder = typeBuilder.DefineMethod("Method",
			                                             MethodAttributes.Static | MethodAttributes.Public,
			                                             expression.ReturnType,
			                                             expression.Parameters.Select(x => x.Type).ToArray());

			expression.CompileToMethod(methodBuilder);
			var type = typeBuilder.CreateType();
			return type.GetMethod(methodBuilder.Name);
		}

		public static IEnumerable<string> FilterUpdatedProperties(string[] properties, BitArray updatedProperties)
		{
			return properties.Where((x, n) => updatedProperties[n]);
		}

		public static void OnCreateMappedEntities(IEnumerable<IMappedEntity> entities)
		{
			foreach (var entity in entities)
			{
				entity.OnCreated();
			}
		}

		public static void AddEntitesOfType(ILoadedEntityContainer stepEntity, RuntimeTypeHandle type, object entity)
		{
			stepEntity.GetLoadedEntities(type).Add(entity);
		}
	}

	public static class EntityGenerator<T> where T : class
	{
		private const MethodAttributes PropertyAccessAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName;

		public static Func<T> Create { get; private set; }

		private static Func<T> CreateDerivedClassByClass(Type tImpl)
		{
			ILGenerator ig;
			Label label;
			var typeBuilder = EntityGenerator.ModuleBuilder.DefineType(
				string.Format("Entity<{0}>", tImpl.FullName),
				TypeAttributes.Class,
				tImpl,
				new[] { typeof(Domain.Interfaces.IFieldTracker), typeof(ILoadedEntityContainer), typeof(IMapperKeyEntity) });

            // adding Serializable attribute
            var customAttributBuilder = new CustomAttributeBuilder(typeof(SerializableAttribute).GetConstructor(Type.EmptyTypes), new object[] { });
            typeBuilder.SetCustomAttribute(customAttributBuilder);

			#region ILoadedEntityContainer
			var loadedEntitiesField = typeBuilder.DefineField("ʬʬLoadedEntities", typeof(Dictionary<RuntimeTypeHandle,List<object>>), FieldAttributes.Private);
			var getLoadedEntitiesMethod = ExpressionUtility<ILoadedEntityContainer>.GetMethodInfo(x => x.GetLoadedEntities(default(RuntimeTypeHandle)));
			var getter = typeBuilder.DefineMethod(getLoadedEntitiesMethod.Name, PropertyAccessAttributes,
			                                      getLoadedEntitiesMethod.ReturnType,
			                                      getLoadedEntitiesMethod.GetParameters().Select(x => x.ParameterType).ToArray());

			ig = getter.GetILGenerator();
			label = ig.DefineLabel();
			
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldfld, loadedEntitiesField);
			ig.Emit(OpCodes.Dup);
			ig.Emit(OpCodes.Brtrue_S, label);
			ig.Emit(OpCodes.Pop);
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Newobj, loadedEntitiesField.FieldType.GetConstructor(Type.EmptyTypes));
			ig.Emit(OpCodes.Stfld, loadedEntitiesField);
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldfld, loadedEntitiesField);
			ig.MarkLabel(label);

			var local = ig.DeclareLocal(typeof(List<object>));
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Ldloca_S, local.LocalIndex);
			ig.Emit(OpCodes.Call, loadedEntitiesField.FieldType.GetMethod("TryGetValue"));
			
			var label2 = ig.DefineLabel();
			ig.Emit(OpCodes.Brtrue_S, label2);
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldfld, loadedEntitiesField);
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Newobj, local.LocalType.GetConstructor(Type.EmptyTypes));
			ig.Emit(OpCodes.Dup);
			ig.Emit(OpCodes.Stloc, local);
			ig.Emit(OpCodes.Call, loadedEntitiesField.FieldType.GetMethod("Add"));
			ig.MarkLabel(label2);
			
			ig.Emit(OpCodes.Ldloc_S, local);
			ig.Emit(OpCodes.Ret);
			typeBuilder.DefineMethodOverride(getter, getLoadedEntitiesMethod);
			#endregion

			#region IMapperKeyEntity
			var mapperKeyField = typeBuilder.DefineField("ʬʬMapperKey", typeof(string), FieldAttributes.Private);
			var setMapperKeyMethod = ExpressionUtility<IMapperKeyEntity>.GetMethodInfo(x => x.SetMapperKey(default(string)));
			var setMapperKeyMethodBuilder = typeBuilder.DefineMethod(setMapperKeyMethod.Name, PropertyAccessAttributes,
									  setMapperKeyMethod.ReturnType,
									  setMapperKeyMethod.GetParameters().Select(x => x.ParameterType).ToArray());
			ig = setMapperKeyMethodBuilder.GetILGenerator();
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Stfld, mapperKeyField);
			ig.Emit(OpCodes.Ret);
			#endregion

			var propertyInfos = tImpl.GetProperties()
				.Where(x => x.CanWrite && x.CanRead && x.GetGetMethod().IsVirtual && x.GetSetMethod().IsVirtual)
				.Where(x => x.PropertyType.IsSimpleType())
				.OrderBy(x => x.Name)
				.ToArray();


			var isTrackingDisabledInfo = ExpressionUtility<Domain.Interfaces.IFieldTracker>.GetPropertyInfo(x => x.IsTrackingDisabled);
			var isTrackingDisabledField = GeneratePropertyOverride(typeBuilder, isTrackingDisabledInfo);
			var getUpdatedFieldsInfo = ExpressionUtility<Domain.Interfaces.IFieldTracker>.GetMethodInfo(x => x.GetUpdatedFields());

			var loadedField = typeBuilder.DefineField("ʬʬPropertiesLoaded", typeof(BitArray), FieldAttributes.Private);
			var updatedField = typeBuilder.DefineField("ʬʬPropertiesUpdated", typeof(BitArray), FieldAttributes.Private);
			var propertiesField = typeBuilder.DefineField("ʬʬProperties", typeof(string[]), FieldAttributes.Public | FieldAttributes.Static);

			#region GetUpdatedFieldsMethod
			ig = typeBuilder.DefineMethod(getUpdatedFieldsInfo.Name, PropertyAccessAttributes, getUpdatedFieldsInfo.ReturnType,
										  getUpdatedFieldsInfo.GetParameters().Select(x => x.ParameterType).ToArray()).GetILGenerator();
			ig.Emit(OpCodes.Ldsfld, propertiesField);
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldfld, updatedField);
			ig.Emit(OpCodes.Call, ((Expression<Func<IEnumerable<string>>>)(() => EntityGenerator.FilterUpdatedProperties(null, null))).Body.MethodInfo());
			ig.Emit(OpCodes.Ret);
			#endregion

			#region Constructor
			ig = typeBuilder.DefineConstructor(
				MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes).GetILGenerator();

			Action<FieldInfo> initBitArrayField = fieldInfo =>
			{
				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldc_I4, propertyInfos.Count());
				ig.Emit(OpCodes.Newobj, typeof(BitArray).GetConstructor(new[] { typeof(int) }));
				ig.Emit(OpCodes.Stfld, fieldInfo);
			};
			initBitArrayField(loadedField);
			initBitArrayField(updatedField);

			foreach (var propertyInfo in (from property in tImpl.GetProperties()
			                              where property.CanWrite && property.CanRead 
			                              where property.PropertyType.IsGenericType
			                              let genericType = property.PropertyType.GetGenericTypeDefinition()
			                              where new[] {typeof (IEnumerable<>), typeof (IList<>), typeof (List<>)}.Contains(genericType)
			                              select property))
			{
				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Newobj, 
					typeof (List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments())
					.GetConstructor(Type.EmptyTypes));
				ig.Emit(OpCodes.Call, propertyInfo.GetSetMethod(true));
			}

			ig.Emit(OpCodes.Ret);
			#endregion

			var exceptionCtor = typeof(PropertyNotLoadedException<T>).GetConstructor(new[] { typeof(string), typeof(string) });
			Debug.Assert(exceptionCtor != null, "exceptionCtor != null");

			for (var index = 0; index < propertyInfos.Length; index++)
			{
				var propertyInfo = propertyInfos[index];
				#region GenereateSetMethod
				ig = typeBuilder.DefineMethod(
					propertyInfo.GetSetMethod().Name,
					PropertyAccessAttributes,
					typeof(void), new Type[] { propertyInfo.PropertyType }).GetILGenerator();
				var setLabel = ig.DefineLabel();
				var updateLabel = ig.DefineLabel();

				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldfld, isTrackingDisabledField);
				ig.Emit(OpCodes.Brtrue_S, setLabel);

				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldfld, loadedField);
				ig.Emit(OpCodes.Ldc_I4, index);
				ig.Emit(OpCodes.Call, typeof(BitArray).GetMethod("Get", new[] { typeof(int) }));
				ig.Emit(OpCodes.Brfalse_S, updateLabel);

				ig.Emit(OpCodes.Ldarg_0);
				ig.EmitCall(OpCodes.Call, propertyInfo.GetGetMethod(), null);
				ig.Emit(OpCodes.Ldarg_1);
				Compare(ig, propertyInfo.PropertyType);
				ig.Emit(OpCodes.Brtrue_S, setLabel);

				Action<FieldInfo> setBitArrayElement = fieldInfo =>
				{
					ig.Emit(OpCodes.Ldarg_0);
					ig.Emit(OpCodes.Ldfld, fieldInfo);
					ig.Emit(OpCodes.Ldc_I4, index);
					ig.Emit(OpCodes.Ldc_I4_1);
					ig.Emit(OpCodes.Call, typeof(BitArray).GetMethod("Set", new[] { typeof(int), typeof(bool) }));
				};

				ig.MarkLabel(updateLabel);
				setBitArrayElement(updatedField);

				ig.MarkLabel(setLabel);
				setBitArrayElement(loadedField);

				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldarg_1);
				ig.EmitCall(OpCodes.Call, propertyInfo.GetSetMethod(), null);
				ig.Emit(OpCodes.Ret);
				#endregion
				#region GenerateGetMethod
				ig = typeBuilder.DefineMethod(
					propertyInfo.GetGetMethod().Name,
					PropertyAccessAttributes,
					propertyInfo.PropertyType, Type.EmptyTypes).GetILGenerator();

				label = ig.DefineLabel();
				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldfld, loadedField);
				ig.Emit(OpCodes.Ldc_I4, index);
				ig.Emit(OpCodes.Call, typeof(BitArray).GetMethod("Get", new[] { typeof(int) }));
				ig.Emit(OpCodes.Brtrue_S, label);
				ig.Emit(OpCodes.Ldstr, propertyInfo.Name);
				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldfld, mapperKeyField);
				ig.Emit(OpCodes.Newobj, exceptionCtor);
				ig.Emit(OpCodes.Throw);
				ig.MarkLabel(label);
				ig.Emit(OpCodes.Ldarg_0);
				ig.EmitCall(OpCodes.Call, propertyInfo.GetGetMethod(), null);
				ig.Emit(OpCodes.Ret);

				#endregion
			}

			var type = typeBuilder.CreateType();

			type.GetField(propertiesField.Name).SetValue(null, propertyInfos.Select(x => x.Name).ToArray());

			var constructorInfo = type.GetConstructor(Type.EmptyTypes);
			Debug.Assert(constructorInfo != null, "constructorInfo != null");

			var dm = new DynamicMethod("Constructor", typeof(T), new Type[0], type);
			ig = dm.GetILGenerator();

			ig.Emit(OpCodes.Newobj, constructorInfo);
			ig.Emit(OpCodes.Ret);

			return (Func<T>)dm.CreateDelegate(typeof(Func<T>));
		}

		private static void Compare(ILGenerator ig, Type type)
		{
			var opMethod = type.GetMethod("op_Equality", new[] {type, type});
			if (opMethod != null)
			{
				ig.EmitCall(OpCodes.Call, opMethod, null);
			}
			else if (type.IsNullable())
			{
				var a = ig.DeclareLocal(type);
				var b = ig.DeclareLocal(type);
				ig.Emit(OpCodes.Stloc, a);
				ig.Emit(OpCodes.Stloc, b);
				var getValueOrDefaultMethod = type.GetMethod("GetValueOrDefault", Type.EmptyTypes);
				ig.Emit(OpCodes.Ldloca, a);
				ig.Emit(OpCodes.Call, getValueOrDefaultMethod);
				ig.Emit(OpCodes.Ldloca, b);
				ig.Emit(OpCodes.Call, getValueOrDefaultMethod);
				var bne = ig.DefineLabel();
				Compare(ig, type.GetGenericArguments()[0]);
				ig.Emit(OpCodes.Ldc_I4_1);
				ig.Emit(OpCodes.Bne_Un, bne);
				var getHasValueMethod = type.GetProperty("HasValue").GetGetMethod();
				ig.Emit(OpCodes.Ldloca, a);
				ig.Emit(OpCodes.Call, getHasValueMethod);
				ig.Emit(OpCodes.Ldloca, b);
				ig.Emit(OpCodes.Call, getHasValueMethod);
				ig.Emit(OpCodes.Ceq);
				var end = ig.DefineLabel();
				ig.Emit(OpCodes.Br, end);
				ig.MarkLabel(bne);
				ig.Emit(OpCodes.Ldc_I4_0);
				ig.MarkLabel(end);
			}
			else
			{
				ig.Emit(OpCodes.Ceq);
			}
		}

		private static Type CreateDerivedClassByInterface()
		{
			ILGenerator ig;

			var type =
				typeof (T).GetInterfaces().Where(x => EntityGenerator.InterfaceMap.ContainsKey(x)).Select(
					x => EntityGenerator.InterfaceMap[x]).FirstOrDefault() ?? typeof (object);

			var typeBuilder = EntityGenerator.ModuleBuilder.DefineType(
				string.Format("ImplOf<{0}>", typeof(T).FullName),
				TypeAttributes.Class,
				type,
				new [] { typeof(T) });

			var propertyInfos = typeof(T).GetProperties()
				.OrderBy(x => x.Name)
				.ToArray();

			foreach (var propertyInfo in propertyInfos)
			{
				GeneratePropertyOverride(typeBuilder, propertyInfo);
			}

			return typeBuilder.CreateType();
		}
		
		static EntityGenerator()
		{
			if(typeof(T).IsInterface)
			{
				var type = CreateDerivedClassByInterface();
				Create = CreateDerivedClassByClass(type);
			}
			else
			{
				Create = CreateDerivedClassByClass(typeof(T));
			}
		}

		private static FieldBuilder GeneratePropertyOverride(TypeBuilder typeBuilder, PropertyInfo propertyToOverride)
		{
			ILGenerator ig;
			var field = typeBuilder.DefineField("ʬ" + propertyToOverride.Name, propertyToOverride.PropertyType, FieldAttributes.Private);
			var propertyBuilder = typeBuilder.DefineProperty(propertyToOverride.Name, PropertyAttributes.None, propertyToOverride.PropertyType, null);

			var getter = typeBuilder.DefineMethod("ʬget_" + propertyToOverride.Name, PropertyAccessAttributes, propertyToOverride.PropertyType,
				                                    Type.EmptyTypes);

			ig = getter.GetILGenerator();
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldfld, field);
			ig.Emit(OpCodes.Ret);
			if (propertyToOverride.CanRead)
			{
				typeBuilder.DefineMethodOverride(getter, propertyToOverride.GetGetMethod());
			}
			propertyBuilder.SetGetMethod(getter);

			var setter = typeBuilder.DefineMethod("ʬset_" + propertyToOverride.Name, PropertyAccessAttributes, null,
													new[] { propertyToOverride.PropertyType });

			ig = setter.GetILGenerator();
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Stfld, field);
			ig.Emit(OpCodes.Ret);
			if (propertyToOverride.CanWrite)
			{
				typeBuilder.DefineMethodOverride(setter, propertyToOverride.GetSetMethod());
			}
			propertyBuilder.SetSetMethod(setter);
			return field;
		}
	}
}